/*
 * Copyright Radosław Smogura, 2012. All rights reserved.
 *
 * The content of this file is confidential and it is intelectual property
 * of copyright owners.
 * 
 *
 * $URL: http://reprap-ide.googlecode.com/svn/trunk/RepRapIDE/RepRapProject/src/main/java/eu/softper/reprap/generator/OutlineGenerator.java $
 * $Id: OutlineGenerator.java 4 2012-11-20 13:18:37Z mail@smogura.eu $
 * $Revision: 4 $
 */
package eu.softper.reprap.generator;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;

/** This generator walks around all outline lines
 *
 * @author rsmogura
 */
public class OutlineGenerator<T extends Number & Comparable<T>> {
    private final T DEG_90;
    private final T DEG_180;
    private final T DEG_270;
    private final T DEG_360;
    
    private static final BigDecimal BD_TWO = BigDecimal.valueOf(2);
    
    private Layer<T> layer;
    private OutlineGeneratorConfig config;
    
    private HashSet<SceneLine<T>> outliens;
    private List<SceneLine<T>> currentShape;
    private List<List<SceneLine<T>>> shapes;
    
    /** Contains all lines in all detected shapes. */
    private HashSet<SceneLine<T>> processedLines = new HashSet<SceneLine<T>>();
    
    private NumericFactory<T> nf;
    
    private DecimalVertex<T> currentVertex;
    
    public OutlineGenerator(NumericFactory<T> numericFactory, Layer layer, OutlineGeneratorConfig config) {
        this.layer = layer;
        this.config = config;
        this.nf = numericFactory;
        
        /* Typically there will be no more then 2-3 shapes, */
        this.shapes = new ArrayList<List<SceneLine<T>>>(3);
        
        DEG_90 = numericFactory.valueOf(90.0);
        DEG_180 = numericFactory.valueOf(180.0);
        DEG_270 = numericFactory.valueOf(270.0);
        DEG_360 = numericFactory.valueOf(360.0);

    }
    
    
    public void generateOutline() {
        separateOutlines();
        if (currentShape != null && currentShape.size() > 2)
            shapes.add(currentShape);
        
        if (!shapes.isEmpty()) {
            List<SceneLine<T>> l = shapes.get(shapes.size() - 1);
            SceneLine ll = l.get(l.size() - 1);
            
            Object o;
            o = layer.getLinesFromVertex().get(ll.getV1());
            o = layer.getLinesFromVertex().get(ll.getV2());
            o.hashCode();
        }
        /* TODO add assertion that all shapes are closed. */
        for (List<SceneLine<T>> shape : shapes) {
            LinkedList<SceneLine<T>> reduced = new LinkedList<SceneLine<T>>();
            reduceOutlineToDiameter(shape, reduced);
            layer.getReducedOutlines().add(reduced);
            shape.clear();
        }
        
        shapes = null;
    }
    
    protected void makeProcessedCurrent(SceneLine sl) {
        double v1 = sl.getV1().getX().doubleValue();
        double v2 = sl.getV2().getX().doubleValue();
        
        if (layer.getzPos().compareTo(new BigDecimal("2.0")) == 0 &&
            Math.abs((14.0 - v1)) < 1 || Math.abs((14.0 - v2)) < 1) {
            sl.hashCode();
        }
        outliens.remove(sl);
        processedLines.add(sl);
        currentShape.add(sl);
    }
    
    protected T lineAngle(SceneLine<T> sl) {
        if (!sl.getXyNormalsAngles().isEmpty()) {
            if (sl.getXyNormalsAngles().size() > 1)
                throw new IllegalStateException();
            return sl.getXyNormalsAngles().iterator().next();
        }
        
        return null;
    }
    
    protected T fixDiff(T diff) {
        if (diff.compareTo(DEG_180) < 0)
            return nf.negate(diff);
        return diff;
    }
    
    protected SceneLine<T> findNextUnprocessed(SceneLine<T> start, DecimalVertex<T> processedVeretx) {
        if (layer.getNumber() == 5 && currentVertex != null && Math.abs(currentVertex.getY().doubleValue() + 39.0) < 1 ) {
                layer.hashCode();
            }
        T lineAngle = lineAngle(start);
        if (lineAngle == null)
            return null;
        
        DecimalVertex<T> searchVertex;
        
        if (currentVertex != null && start.getV1().equals(processedVeretx))
            searchVertex = start.getV2();
        else
            searchVertex = start.getV1();
        
        SceneLine<T> result = findNextUnprocessed(start, layer.getLinesFromVertex().get(searchVertex));
        currentVertex = searchVertex;
        return result;
    }
    
    /** Finds next unprocessed lines checking line X-Y normal.
     * This method uses CCW line going, to find most biggest shape.
     * @param start
     * @param lines
     * @return 
     */
    protected SceneLine<T> findNextUnprocessed(SceneLine<T> start, Collection<SceneLine<T>> lines) {
        SceneLine bestLine = null;
        T bestLineAngle = null;
        T bestDiff = null;
        for (SceneLine nl : lines) {
            /* Line belongs to other shape */
            if (processedLines.contains(nl))
                continue;
            
            final SceneLineType lineType = nl.getLineType();
            if (lineType != null && lineType != SceneLineType.OUTLINE)
                continue;
            
            /* Actually line should be in process lines list, but for safty */
            if (start.equals(nl))
                continue;
            
            T na = lineAngle(nl);
            if (na == null)
                continue;

            na = nf.degreeAsRemainder(na);
            if (bestLine == null) {                
                bestLineAngle = na;
                if (bestLineAngle == null)
                    continue;
                bestLine = nl;
                bestDiff = fixDiff(nf.degreeAsRemainder(nf.subs(bestLineAngle,na)));
                continue;
            }

            /* For measuirng _diff_ angles we use CCW approach. */
            /* After earlier (not written) steps each line should have at most one angle. */
            if (na == null)
                continue;
            
            /* This will generate largest area.
             * Consider searching smallest area, this may add some outlines
             * printed inside shape, but will simplify other considerations,
             * Cosnider following
             * _________
             * |\      |
             * | \     |
             * |  \    |
             * |   \   |
             * |    \  |
             * |     \ |
             * ---------
             * There are two diagonals (two normal angles |a_1 - a_2| = 180
             * 
             * _____
             * \   /
             *  \ /
             *  / \
             * /   \
             * -----
             * Center point contains 4 lines from veritce (triangle fan)
             * 
             * Searching for largest area will generate square and "clepsydra".
             * Squery will not contain inner outlines when printed, but
             * clepsydra may be generated incorrectly 
             * (or must be dequpled again).
             * 
             * Searching for smallest region will generate two traingles for
             * both shapes, simple outline processing (reducing shape by half
             * of outline diameter) will make that square will be printed
             * with inner outline diagonal (is it wrong?), but clepsydre
             * will be generated correctly
             * 
             */
            T diff = fixDiff(nf.degreeAsRemainder(nf.subs(bestLineAngle,na)));
            
            if (bestDiff == null) {
                bestDiff = diff;
                bestLineAngle = na;
                bestLine = nl;
            }
            
//            if (diff.compareTo(DEG_180) < 0) {
//                if (diff.compareTo(bestDiff) < 0) {
//                    bestDiff = diff;
//                    bestLineAngle = na;
//                    bestLine = nl;
//                }
//            }else {
               if (diff.compareTo(bestDiff) > 0) {
                    bestDiff = diff;
                    bestLineAngle = na;
                    bestLine = nl;
                } 
//            }
        }
        
        return bestLine;
    }
    
    protected SceneLine startNewShape() {
        if (outliens.isEmpty())
            return null;
        
        SceneLine currentLine = outliens.iterator().next();
        if (currentShape != null && currentShape.isEmpty()) {
            throw new AssertionError();
        }else {
            if (currentShape != null && currentShape.size() > 2)
               shapes.add(currentShape);
            else
                shapes.hashCode();
        }
        currentShape = new LinkedList<SceneLine<T>>();
        makeProcessedCurrent(currentLine);
        
        return currentLine;
    }
    
    protected void separateOutlines() {
        /* This will separate outlines so it will create closed lines
         * but not always shapes.
         * 
         * In some cases separated outlines are not good for direct reducing
         * infill by radius - consider 60 degree 2 triangles fan connected
         * in center (0;0)
         */
        
        /* Create local copy of scene lines. */
        outliens =  new HashSet<SceneLine<T>>(layer.getOutlines());
        if (outliens.isEmpty())
            return;
        
        SceneLine<T> currentLine = startNewShape();
        
        while (!outliens.isEmpty()) {
            SceneLine nextLine = findNextUnprocessed(currentLine, currentVertex);
            
            if (layer.getNumber() == 11) {
                layer.hashCode();
            }
            if (nextLine != null) {
                if (nextLine.getLineType() != null && nextLine.getLineType() != SceneLineType.OUTLINE) {
                    outliens.remove(nextLine);
                    processedLines.add(nextLine);
                    currentVertex = null;
                    continue;
                }
                makeProcessedCurrent(nextLine);
                currentLine = nextLine;
            }else {
                currentLine = startNewShape();
                currentVertex = null;
            }
        }
    }
    
    
    protected DecimalVertex<T> makeLine(T pAngle, T nAngle, double  radius, DecimalVertex<T> connVertex) {
        double mAngle = (pAngle.doubleValue() + nAngle.doubleValue()) / 2.0;
        double mAngleDegree = mAngle;
        mAngle = mAngle * Math.PI / 180.0;
        if (nf.abs(nf.reminder(nf.subs(pAngle, nAngle), DEG_360)).compareTo(DEG_180) < 0) {
            mAngle += Math.PI;
        }

        double halfAngle = pAngle.doubleValue() - mAngleDegree;
        halfAngle %= 90.0;
        halfAngle = Math.abs(halfAngle);

        if (nf.abs(nf.reminder(nf.subs(pAngle, nAngle), DEG_360)).compareTo(DEG_180) < 0) {
            halfAngle = 90.0 - halfAngle;
        }

        if (halfAngle == 0.0)
            halfAngle = 90.0;


        if (halfAngle < 0.0 || halfAngle > 90.0) {
            halfAngle = halfAngle;
        }

        halfAngle = halfAngle * Math.PI / 180.0;

        double l = radius / Math.sin(halfAngle);
        
        DecimalVertex<T> result = new DecimalVertex<T>(
                    nf.valueOf(connVertex.getX().doubleValue() +
                        Math.cos(mAngle) * l), 
                    nf.valueOf(connVertex.getY().doubleValue() +
                        Math.sin(mAngle) * l), 
                    connVertex.getZ());
        return result;
    }
    
    protected void reduceOutlineToDiameter(List<SceneLine<T>> separated, List<SceneLine<T>> newOutliens) {
        final double radius = 0.5;
        
        final Iterator<SceneLine<T>> i = separated.iterator();
        final SceneLine<T> first = i.next();
        final T firstAngle = lineAngle(first);
        
        DecimalVertex<T> firstVertex = null;
        
        SceneLine<T> pLine = first;
        T pAngle = firstAngle;
        
        SceneLine<T> nLine = null;
        T nAngle = null;
        
        DecimalVertex prevVertex = null;
        while (i.hasNext()) {
            nLine = i.next();
            nAngle = lineAngle(nLine);
            
            DecimalVertex<T> connVeretx = findConnectionVertex(pLine, nLine);
            if (nLine.getMinY().doubleValue() <= 24.0 && nLine.getMinY().doubleValue() >= 23.0 && layer.getNumber() == 11) {
                i.hasNext();
            }
            
            if (connVeretx == null)
                nLine.hashCode();
            
            if (layer.getNumber() == 6 && connVeretx.getY().doubleValue() >= -31.8 && connVeretx.getY().doubleValue() <= -30.8) {
                new Object().hashCode();
            }
            
            connVeretx = makeLine(pAngle, nAngle, radius, connVeretx);

            if (prevVertex != null) {
                SceneLine<T> newLine = new SceneLine<T>(prevVertex, connVeretx);
                newLine.getXyNormalsAngles().addAll(pLine.getXyNormalsAngles());
                newOutliens.add(newLine);
            }else {
                firstVertex = connVeretx;
            }
            
            prevVertex = connVeretx;
            pLine = nLine;
            pAngle = nAngle;
        }
        
        /* Now connect last line with first line. */  
        DecimalVertex<T> connVeretx = findConnectionVertex(first, nLine);
        
        double mAngle = (firstAngle.doubleValue() + nAngle.doubleValue()) / 2.0;
        mAngle = mAngle * Math.PI / 180.0;
        
        if (nf.abs(nf.reminder(nf.subs(firstAngle, nAngle), DEG_360)).compareTo(DEG_180) < 0) {
            mAngle += Math.PI;
        }
            
        if (connVeretx == null) {
            nAngle.hashCode();
            return;
        }
        
        connVeretx = makeLine(firstAngle, nAngle, radius, connVeretx);

        SceneLine<T> newLine = new SceneLine<T>(prevVertex, connVeretx);
        newLine.getXyNormalsAngles().addAll(pLine.getXyNormalsAngles());
        newOutliens.add(newLine);
        
        newLine = new SceneLine<T>(firstVertex, connVeretx);
        newLine.getXyNormalsAngles().addAll(first.getXyNormalsAngles());
        newOutliens.add(newLine);
    }
    
    public static DecimalVertex findConnectionVertex(SceneLine l1, SceneLine l2) {
        if (l1.getV1().equals(l2.getV1())) {
            return l1.getV1();
        } else if (l1.getV1().equals(l2.getV2())) {
            return l1.getV1();
        } else if (l1.getV2().equals(l2.getV1())) {
            return l1.getV2();
        } else if (l1.getV2().equals(l2.getV2())) {
            return l1.getV2();
        } else {
            return null;
        }
    }
}
